{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试滑块验证码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''导入模块'''\n",
    "from DrissionPage import ChromiumPage\n",
    "import ddddocr\n",
    "import time\n",
    "page = ChromiumPage()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''删除img文件夹'''\n",
    "import os\n",
    "import shutil\n",
    "\n",
    "def delete_img_folder():\n",
    "    folder_name = 'img'\n",
    "\n",
    "    # 获取当前工作目录\n",
    "    current_directory = os.getcwd()\n",
    "\n",
    "    # 构造要删除的文件夹的完整路径\n",
    "    folder_path = os.path.join(current_directory, folder_name)\n",
    "\n",
    "    try:\n",
    "        # 删除文件夹及其内容\n",
    "        shutil.rmtree(folder_path)\n",
    "        # print(f\"成功删除文件夹: {folder_path}\")\n",
    "    except FileNotFoundError:\n",
    "        # print(f\"文件夹 '{folder_path}' 不存在\")\n",
    "        pass\n",
    "    except Exception as e:\n",
    "        print(f\"发生错误: {e}\")\n",
    "\n",
    "# 调用函数以删除'img'文件夹\n",
    "delete_img_folder()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''代码计算缺口距离'''\n",
    "import PIL\n",
    "import cv2\n",
    "import numpy as np\n",
    "from pathlib import Path\n",
    "def pil_to_cv2(img):\n",
    "    \"\"\"\n",
    "    pil转cv2图片\n",
    "    :param img: pil图像, <type 'PIL.JpegImagePlugin.JpegImageFile'>\n",
    "    :return: cv2图像, <type 'numpy.ndarray'>\n",
    "    \"\"\"\n",
    "    img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)\n",
    "    return img\n",
    "\n",
    "\n",
    "def bytes_to_cv2(img):\n",
    "    \"\"\"\n",
    "    二进制图片转cv2\n",
    "    :param img: 二进制图片数据, <type 'bytes'>\n",
    "    :return: cv2图像, <type 'numpy.ndarray'>\n",
    "    \"\"\"\n",
    "    # 将图片字节码bytes, 转换成一维的numpy数组到缓存中\n",
    "    img_buffer_np = np.frombuffer(img, dtype=np.uint8)\n",
    "    # 从指定的内存缓存中读取一维numpy数据, 并把数据转换(解码)成图像矩阵格式\n",
    "    img_np = cv2.imdecode(img_buffer_np, 1)\n",
    "    return img_np\n",
    "\n",
    "\n",
    "def cv2_open(img, flag=None):\n",
    "    \"\"\"\n",
    "    统一输出图片格式为cv2图像, <type 'numpy.ndarray'>\n",
    "    :param img: <type 'bytes'/'numpy.ndarray'/'str'/'Path'/'PIL.JpegImagePlugin.JpegImageFile'>\n",
    "    :param flag: 颜色空间转换类型, default: None\n",
    "        eg: cv2.COLOR_BGR2GRAY（灰度图）\n",
    "    :return: cv2图像, <numpy.ndarray>\n",
    "    \"\"\"\n",
    "    if isinstance(img, bytes):\n",
    "        img = bytes_to_cv2(img)\n",
    "    elif isinstance(img, (str, Path)):\n",
    "        img = cv2.imread(str(img))\n",
    "    elif isinstance(img, np.ndarray):\n",
    "        img = img\n",
    "    elif isinstance(img, PIL.Image):\n",
    "        img = pil_to_cv2(img)\n",
    "    else:\n",
    "        raise ValueError(f'输入的图片类型无法解析: {type(img)}')\n",
    "    if flag is not None:\n",
    "        img = cv2.cvtColor(img, flag)\n",
    "    return img\n",
    "\n",
    "\n",
    "def get_distance(bg, tp, save_path=None):\n",
    "    \"\"\"\n",
    "    :param bg: 背景图路径或Path对象或图片二进制\n",
    "        eg: 'assets/bg.jpg'\n",
    "            Path('assets/bg.jpg')\n",
    "    :param tp: 缺口图路径或Path对象或图片二进制\n",
    "        eg: 'assets/tp.jpg'\n",
    "            Path('assets/tp.jpg')\n",
    "    :param save_path: 保存路径, <type 'str'/'Path'>; default: None\n",
    "    :return: 缺口位置\n",
    "    \"\"\"\n",
    "    # 读取图片\n",
    "    bg_gray = cv2_open(bg, flag=cv2.COLOR_BGR2GRAY)\n",
    "    tp_gray = cv2_open(tp, flag=cv2.COLOR_BGR2GRAY)\n",
    "    # 边缘检测\n",
    "    tp_gray = cv2.Canny(tp_gray, 255, 255)\n",
    "    bg_gray = cv2.Canny(bg_gray, 255, 255)\n",
    "    # 目标匹配\n",
    "    result = cv2.matchTemplate(bg_gray, tp_gray, cv2.TM_CCOEFF_NORMED)\n",
    "    # 解析匹配结果\n",
    "    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)\n",
    "    distance = max_loc[0] + 8\n",
    "    if save_path:\n",
    "        # 需要绘制的方框高度和宽度\n",
    "        tp_height, tp_width = tp_gray.shape[:2]\n",
    "        # 矩形左上角点位置\n",
    "        x, y = max_loc\n",
    "        # 矩形右下角点位置\n",
    "        _x, _y = x + tp_width, y + tp_height\n",
    "        # 绘制矩形\n",
    "        bg_img = cv2_open(bg)\n",
    "        cv2.rectangle(bg_img, (x, y), (_x, _y), (0, 0, 255), 2)\n",
    "        # 保存缺口识别结果到背景图\n",
    "        if save_path:\n",
    "            save_path = Path(save_path).resolve()\n",
    "            save_path = save_path.parent / f\"{save_path.stem}.{distance}{save_path.suffix}\"\n",
    "            save_path = save_path.__str__()\n",
    "            cv2.imwrite(save_path, bg_img)\n",
    "    return distance\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''使用ddddocr计算缺口距离'''\n",
    "def get_distance_by_ddddocr():\n",
    "    '''\n",
    "    直接使用img文件夹下的target.png和background.png\n",
    "    target.png：指的是小滑块图片\n",
    "    background.png：带坑位的背景图\n",
    "    '''\n",
    "    det = ddddocr.DdddOcr(det=False, ocr=False)\n",
    "    with open('./img/target.png', 'rb') as f:\n",
    "        target_bytes = f.read()\n",
    "    with open('./img/background.png', 'rb') as f:\n",
    "        background_bytes = f.read()\n",
    "    res = det.slide_match(target_bytes, background_bytes)\n",
    "    x_distance = res[\"target\"][0]\n",
    "    return x_distance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''据偏移量获取移动轨迹'''\n",
    "import random\n",
    "def get_tracks(distance):\n",
    "    \"\"\"\n",
    "\n",
    "    :param distance: 缺口距离\n",
    "    :return: 轨迹\n",
    "    \"\"\"\n",
    "    # 分割加减速路径的阀值\n",
    "    value = round(random.uniform(0.55, 0.75), 2)\n",
    "    # 划过缺口 20 px\n",
    "    #distance += 20\n",
    "    # 初始速度，初始计算周期， 累计滑动总距\n",
    "    v, t, sum = 0, 0.3, 0\n",
    "    # 轨迹记录\n",
    "    plus = []\n",
    "    # 将滑动记录分段，一段加速度，一段减速度\n",
    "    mid = distance * value\n",
    "    while sum < distance:\n",
    "        if sum < mid:\n",
    "            # 指定范围随机产生一个加速度\n",
    "            a = round(random.uniform(2.5, 3.5), 1)\n",
    "        else:\n",
    "            # 指定范围随机产生一个减速的加速度\n",
    "            a = -round(random.uniform(2.0, 3.0), 1)\n",
    "        s = v * t + 0.5 * a * (t ** 2)\n",
    "        v = v + a * t\n",
    "        sum += s\n",
    "        plus.append(round(s))\n",
    "\n",
    "    # end_s = sum - distance\n",
    "    # plus.append(round(-end_s))\n",
    "\n",
    "    # 手动制造回滑的轨迹累积20px\n",
    "    # reduce = [-3, -3, -2, -2, -2, -2, -2, -1, -1, -1]\n",
    "    reduce = [-6, -4, -6, -4]\n",
    "    return {'plus': plus, 'reduce': reduce}\n",
    "\n",
    "def move_to_gap(slide_ele,tracks):\n",
    "    # 添加行动链，向右并超出一部分\n",
    "    page.actions.hold(f\"{slide_ele}\")\n",
    "    for track in tracks['plus']:\n",
    "        page.actions.move(\n",
    "            offset_x=track,\n",
    "            offset_y=round(random.uniform(1.0, 3.0), 0),\n",
    "            duration=.1\n",
    "        )\n",
    "    time.sleep(0.5)\n",
    "    '''\n",
    "    # 添加行动链，返回超出的部分\n",
    "    for back_tracks in tracks['reduce']:\n",
    "        page.actions.move(\n",
    "            offset_x=back_tracks,\n",
    "            offset_y=round(random.uniform(1.0, 3.0),0),\n",
    "            duration=.1\n",
    "        )\n",
    "    '''\n",
    "    page.actions.release(f\"{slide_ele}\")\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''嵌入式滑块'''\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=嵌入式(embed)\").click()\n",
    "page.ele('@name=account').input(\"test\")\n",
    "page.ele('@name=password').input(\"test\")\n",
    "page.ele('.shumei_captcha_loaded_img_fg').save(path=\"./img/\",name='target.png')\n",
    "page.ele('.shumei_captcha_loaded_img_bg').save(path=\"./img/\",name='background.png')\n",
    "# 在网址上计算的距离必须除2，可能是分辨率导致的\n",
    "\n",
    "x_distance =  get_distance_by_ddddocr() / 2\n",
    "trajectory = get_tracks(x_distance)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn_icon sm-iconfont\",tracks = trajectory)\n",
    "time.sleep(0.2)\n",
    "gugu = page.ele('#shumei_form_captcha_wrapper')\n",
    "if gugu:\n",
    "    gugu.get_screenshot(path=\"./img/captcha.png\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 90,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''浮动式(float)'''\n",
    "# 会存在滑块不会出现的情况。\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=浮动式(float)\").click()\n",
    "#  不能使用input()，要不然验证码不会显示\n",
    "page.actions.click('@name=password').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.actions.click('@name=account').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.actions.move_to(\".shumei_captcha_slide_btn_icon sm-iconfont\")\n",
    "page.ele('.shumei_captcha_loaded_img_fg').save(path=\"./img/\",name='target.png')\n",
    "page.ele('.shumei_captcha_loaded_img_bg').save(path=\"./img/\",name='background.png')\n",
    "time.sleep(0.2)\n",
    "# 在网址上计算的距离必须除2，可能是分辨率导致的\n",
    "x_distance =  get_distance_by_ddddocr() / 2\n",
    "trajectory = get_tracks(x_distance-3)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn_icon sm-iconfont\",tracks = trajectory)\n",
    "gugu = page.ele('#shumei_form_captcha_wrapper')\n",
    "if gugu:\n",
    "    gugu.get_screenshot(path=\"./img/captcha.png\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 91,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''弹出式(popup)'''\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=弹出式(popup)\").click()\n",
    "#  不能使用input()，要不然验证码不会显示\n",
    "page.actions.click('@name=account').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.actions.click('@name=password').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.ele('.shumei_login_btn').click()\n",
    "page.ele('.shumei_captcha_loaded_img_fg').save(path=\"./img/\",name='target.png')\n",
    "page.ele('.shumei_captcha_loaded_img_bg').save(path=\"./img/\",name='background.png')\n",
    "time.sleep(0.2)\n",
    "# 在网址上计算的距离必须除2，可能是分辨率导致的\n",
    "x_distance =  get_distance_by_ddddocr() / 2\n",
    "trajectory = get_tracks(x_distance-2)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn_icon sm-iconfont\",tracks = trajectory)\n",
    "# 弹出式验证成功后，弹窗会消失。有概率不能够截到操作后的弹窗。\n",
    "test = page.ele('@class$mode-slide',timeout=0.5)\n",
    "if test:\n",
    "    test.get_screenshot(path=\"./img/captcha.png\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 92,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''自定义样式'''\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=自定义样式\").click()\n",
    "#  不能使用input()，要不然验证码不会显示\n",
    "page.actions.click('@name=account').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.actions.click('@name=password').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.ele('.shumei_login_btn').click()\n",
    "page.ele('.shumei_captcha_loaded_img_fg').save(path=\"./img/\",name='target.png')\n",
    "page.ele('.shumei_captcha_loaded_img_bg').save(path=\"./img/\",name='background.png')\n",
    "time.sleep(0.2)\n",
    "# 在网址上计算的距离必须除2，可能是分辨率导致的\n",
    "x_distance =  get_distance_by_ddddocr() / 2\n",
    "trajectory = get_tracks(x_distance-2)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn\",tracks = trajectory)\n",
    "# 弹出式验证成功后，弹窗会消失。有概率不能够截到操作后的弹窗。\n",
    "test = page.ele('@class$mode-slide',timeout=0.5)\n",
    "if test:\n",
    "    test.get_screenshot(path=\"./img/captcha.png\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 93,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''无图直接滑动'''\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=无图直接滑动\").click()\n",
    "#  不能使用input()，要不然验证码不会显示\n",
    "page.actions.click('@name=account').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.actions.click('@name=password').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.ele('.shumei_login_btn').click()\n",
    "x_distance = (300-40)\n",
    "trajectory = get_tracks(x_distance)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn_icon sm-iconfont\",tracks = trajectory)\n",
    "# page.actions.hold('.shumei_captcha_slide_btn')\n",
    "# page.actions.move(offset_x=move_wight,offset_y=0,duration=.5)\n",
    "# page.actions.release('.shumei_captcha_slide_btn')\n",
    "# time.sleep(0.5)\n",
    "test = page.ele('@class$mode-auto_slide',timeout=0.5)\n",
    "if test:\n",
    "    test.get_screenshot(path=\"./img/captcha.png\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''展示浏览器环境语言'''\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=展示浏览器环境语言\").click()\n",
    "#  不能使用input()，要不然验证码不会显示\n",
    "page.actions.click('@name=account').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.actions.click('@name=password').type(\"test\")\n",
    "time.sleep(0.1)\n",
    "page.ele('.shumei_login_btn').click()\n",
    "page.ele('.shumei_captcha_loaded_img_fg').save(path=\"./img/\",name='target.png')\n",
    "page.ele('.shumei_captcha_loaded_img_bg').save(path=\"./img/\",name='background.png')\n",
    "time.sleep(0.2)\n",
    "# 在网址上计算的距离必须除2，可能是分辨率导致的\n",
    "x_distance =  get_distance_by_ddddocr() / 2\n",
    "trajectory = get_tracks(x_distance)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn_icon sm-iconfont\",tracks = trajectory)\n",
    "# 弹出式验证成功后，弹窗会消失。有概率不能够截到操作后的弹窗。\n",
    "test = page.ele('@class$mode-slide',timeout=0.5)\n",
    "if test:\n",
    "    test.get_screenshot(path=\"./img/captcha.png\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''使用get_src()传参'''\n",
    "page.get(\"https://castatic.fengkongcloud.cn/pr/v1.0.4/demo.html\")\n",
    "delete_img_folder()\n",
    "demo = page.ele(\"@text()=返回demo列表\",timeout=1)\n",
    "if demo:\n",
    "    demo.click()\n",
    "page.ele(\"@text()=嵌入式(embed)\").click()\n",
    "page.ele('@name=account').input(\"test\")\n",
    "page.ele('@name=password').input(\"test\")\n",
    "target_bytes = page.ele('.shumei_captcha_loaded_img_fg').get_src()\n",
    "background_bytes = page.ele('.shumei_captcha_loaded_img_bg').get_src()\n",
    "# 在网址上计算的距离必须除2，可能是分辨率导致的\n",
    "det = ddddocr.DdddOcr(det=False, ocr=False)\n",
    "res = det.slide_match(target_bytes, background_bytes)\n",
    "x_distance = res[\"target\"][0] /2\n",
    "\n",
    "trajectory = get_tracks(x_distance)\n",
    "move_to_gap(slide_ele =\".shumei_captcha_slide_btn_icon sm-iconfont\",tracks = trajectory)\n",
    "time.sleep(0.2)\n",
    "gugu = page.ele('#shumei_form_captcha_wrapper')\n",
    "if gugu:\n",
    "    gugu.get_screenshot(path=\"./img/captcha.png\")\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
